summaryrefslogtreecommitdiff
path: root/app/[lng]
diff options
context:
space:
mode:
Diffstat (limited to 'app/[lng]')
-rw-r--r--app/[lng]/auth/reset-password/page.tsx47
-rw-r--r--app/[lng]/evcp/(evcp)/b-rfq/page.tsx3
-rw-r--r--app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx18
-rw-r--r--app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx65
-rw-r--r--app/[lng]/evcp/(evcp)/evaluation/page.tsx19
-rw-r--r--app/[lng]/evcp/(evcp)/system/layout.tsx11
-rw-r--r--app/[lng]/evcp/(evcp)/system/password-policy/page.tsx63
-rw-r--r--app/[lng]/partners/(partners)/document-list/layout.tsx5
-rw-r--r--app/[lng]/partners/(partners)/settings/layout.tsx68
-rw-r--r--app/[lng]/partners/(partners)/settings/page.tsx224
-rw-r--r--app/[lng]/partners/(partners)/settings/preferences/page.tsx17
-rw-r--r--app/[lng]/partners/(partners)/system/layout.tsx71
-rw-r--r--app/[lng]/partners/(partners)/system/page.tsx62
-rw-r--r--app/[lng]/partners/(partners)/system/permissions/page.tsx17
-rw-r--r--app/[lng]/partners/(partners)/system/roles/page.tsx68
-rw-r--r--app/[lng]/privacy/page.tsx5
16 files changed, 673 insertions, 90 deletions
diff --git a/app/[lng]/auth/reset-password/page.tsx b/app/[lng]/auth/reset-password/page.tsx
new file mode 100644
index 00000000..f49e5d86
--- /dev/null
+++ b/app/[lng]/auth/reset-password/page.tsx
@@ -0,0 +1,47 @@
+// app/[lng]/auth/reset-password/page.tsx
+
+import { redirect } from 'next/navigation';
+import { validateResetTokenAction } from '@/lib/users/auth/partners-auth';
+import InvalidTokenPage from '@/components/login/InvalidTokenPage';
+import ResetPasswordForm from '@/components/login/reset-password';
+import { getPasswordPolicy } from '@/lib/users/auth/passwordUtil';
+
+interface Props {
+ searchParams: { token?: string };
+}
+
+export default async function ResetPasswordPage({ searchParams }: Props) {
+ const token = searchParams.token;
+
+ // 토큰이 없는 경우 로그인 페이지로 리다이렉트
+ if (!token) {
+ redirect('/partners');
+ }
+
+ // 서버에서 토큰 검증
+ const tokenValidation = await validateResetTokenAction(token);
+
+ // 토큰이 유효하지 않은 경우
+ if (!tokenValidation.valid) {
+ return (
+ <InvalidTokenPage
+ expired={tokenValidation.expired || false}
+ error={tokenValidation.error}
+ />
+ );
+ }
+
+ // 패스워드 정책 로드
+ const passwordPolicy = await getPasswordPolicy();
+
+ // 유효한 토큰인 경우 폼 표시
+ return (
+ <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+ <ResetPasswordForm
+ token={token}
+ userId={tokenValidation.userId!}
+ passwordPolicy={passwordPolicy}
+ />
+ </div>
+ );
+} \ No newline at end of file
diff --git a/app/[lng]/evcp/(evcp)/b-rfq/page.tsx b/app/[lng]/evcp/(evcp)/b-rfq/page.tsx
index 213e9127..a66d7b58 100644
--- a/app/[lng]/evcp/(evcp)/b-rfq/page.tsx
+++ b/app/[lng]/evcp/(evcp)/b-rfq/page.tsx
@@ -46,6 +46,8 @@ export default async function PQReviewPage(props: PQReviewPageProps) {
})
])
+ console.log(search, "견적")
+
return (
<Shell className="gap-4">
<div className="flex items-center justify-between space-y-2">
@@ -60,7 +62,6 @@ export default async function PQReviewPage(props: PQReviewPageProps) {
{/* Items처럼 직접 테이블 렌더링 */}
<React.Suspense
- key={JSON.stringify(searchParams)} // URL 파라미터가 변경될 때마다 강제 리렌더링
fallback={
<DataTableSkeleton
columnCount={8}
diff --git a/app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx b/app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx
index 398005fa..a660c492 100644
--- a/app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx
+++ b/app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx
@@ -32,6 +32,24 @@ async function EvaluationCriteriaPage(props: EvaluationCriteriaPageProps) {
return (
<Shell className="gap-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ 협력업체 평가기준표
+ </h2>
+ <p className="text-muted-foreground">
+ 협력업체 평가에 사용되는 평가기준표를 관리{" "}
+ {/* <span className="inline-flex items-center whitespace-nowrap">
+ <Ellipsis className="size-3" />
+ <span className="ml-1">버튼</span>
+ </span>
+ 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */}
+ </p>
+ </div>
+ </div>
+ </div>
+
<Suspense fallback={<Skeleton className="h-7 w-52" />}>
{/* <DateRangePicker
triggerSize="sm"
diff --git a/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx b/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx
index d60f695a..088ae75b 100644
--- a/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx
+++ b/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx
@@ -26,66 +26,7 @@ interface EvaluationTargetsPageProps {
searchParams: Promise<SearchParams>
}
-// 프로세스 안내 팝오버 컴포넌트
-function ProcessGuidePopover() {
- return (
- <Popover>
- <PopoverTrigger asChild>
- <Button variant="ghost" size="icon" className="h-6 w-6">
- <HelpCircle className="h-4 w-4 text-muted-foreground" />
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-96" align="start">
- <div className="space-y-3">
- <div className="space-y-1">
- <h4 className="font-medium">평가 대상 확정 프로세스</h4>
- <p className="text-sm text-muted-foreground">
- 발주실적을 기반으로 평가 대상을 확정하는 절차입니다.
- </p>
- </div>
- <div className="space-y-3 text-sm">
- <div className="flex gap-3">
- <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
- 1
- </div>
- <div>
- <p className="font-medium">발주실적 기반 자동 추출</p>
- <p className="text-muted-foreground">전년도 10월 ~ 해당년도 9월 발주실적에서 업체 목록을 자동으로 생성합니다.</p>
- </div>
- </div>
- <div className="flex gap-3">
- <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
- 2
- </div>
- <div>
- <p className="font-medium">담당자 지정</p>
- <p className="text-muted-foreground">각 평가 대상별로 5개 부서(발주/조달/품질/설계/CS)의 담당자를 지정합니다.</p>
- </div>
- </div>
- <div className="flex gap-3">
- <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
- 3
- </div>
- <div>
- <p className="font-medium">검토 및 의견 수렴</p>
- <p className="text-muted-foreground">모든 담당자가 평가 대상 적합성을 검토하고 의견을 제출합니다.</p>
- </div>
- </div>
- <div className="flex gap-3">
- <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
- 4
- </div>
- <div>
- <p className="font-medium">최종 확정</p>
- <p className="text-muted-foreground">모든 담당자 의견이 일치하면 평가 대상으로 최종 확정됩니다.</p>
- </div>
- </div>
- </div>
- </div>
- </PopoverContent>
- </Popover>
- )
-}
+
export default async function EvaluationTargetsPage(props: EvaluationTargetsPageProps) {
const searchParams = await props.searchParams
@@ -131,7 +72,7 @@ export default async function EvaluationTargetsPage(props: EvaluationTargetsPage
<Badge variant="outline" className="text-sm">
{currentEvaluationYear}년도
</Badge>
- <ProcessGuidePopover />
+
</div>
</div>
</div>
@@ -162,10 +103,12 @@ export default async function EvaluationTargetsPage(props: EvaluationTargetsPage
/>
}
>
+ {currentEvaluationYear &&
<EvaluationTargetsTable
promises={promises}
evaluationYear={currentEvaluationYear}
/>
+}
</React.Suspense>
</Shell>
)
diff --git a/app/[lng]/evcp/(evcp)/evaluation/page.tsx b/app/[lng]/evcp/(evcp)/evaluation/page.tsx
index 3ae3272a..ead61077 100644
--- a/app/[lng]/evcp/(evcp)/evaluation/page.tsx
+++ b/app/[lng]/evcp/(evcp)/evaluation/page.tsx
@@ -17,6 +17,8 @@ import {
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { PeriodicEvaluationsTable } from "@/lib/evaluation/table/evaluation-table"
+import { getPeriodicEvaluations } from "@/lib/evaluation/service"
+import { searchParamsEvaluationsCache } from "@/lib/evaluation/validation"
export const metadata: Metadata = {
title: "협력업체 정기평가",
@@ -93,25 +95,11 @@ function getDefaultEvaluationYear() {
return new Date().getFullYear()
}
-function searchParamsPeriodicEvaluationsCache() {
- // TODO: 실제 파서 구현
- return {
- parse: (params: any) => params
- }
-}
-async function getPeriodicEvaluations(params: any) {
- // TODO: 실제 API 호출 구현
- return {
- data: [],
- total: 0,
- pageCount: 0
- }
-}
export default async function PeriodicEvaluationsPage(props: PeriodicEvaluationsPageProps) {
const searchParams = await props.searchParams
- const search = searchParamsPeriodicEvaluationsCache().parse(searchParams)
+ const search = searchParamsEvaluationsCache.parse(searchParams)
const validFilters = getValidFilters(search.filters || [])
// 기본 필터 처리
@@ -150,7 +138,6 @@ export default async function PeriodicEvaluationsPage(props: PeriodicEvaluations
<Badge variant="outline" className="text-sm">
{currentEvaluationYear}년도
</Badge>
- <ProcessGuidePopover />
</div>
</div>
</div>
diff --git a/app/[lng]/evcp/(evcp)/system/layout.tsx b/app/[lng]/evcp/(evcp)/system/layout.tsx
index 62f3e845..7e8f69d0 100644
--- a/app/[lng]/evcp/(evcp)/system/layout.tsx
+++ b/app/[lng]/evcp/(evcp)/system/layout.tsx
@@ -28,7 +28,7 @@ export default async function SettingsLayout({
const sidebarNavItems = [
{
- title: "SHI Users",
+ title: "삼성중공업 사용자",
href: `/${lng}/evcp/system`,
},
{
@@ -36,13 +36,18 @@ export default async function SettingsLayout({
href: `/${lng}/evcp/system/roles`,
},
{
- title: "Permissions",
+ title: "권한 통제",
href: `/${lng}/evcp/system/permissions`,
},
{
- title: "Vendor Users",
+ title: "협력업체 사용자",
href: `/${lng}/evcp/system/admin-users`,
},
+
+ {
+ title: "비밀번호 정책",
+ href: `/${lng}/evcp/system/password-policy`,
+ },
]
diff --git a/app/[lng]/evcp/(evcp)/system/password-policy/page.tsx b/app/[lng]/evcp/(evcp)/system/password-policy/page.tsx
new file mode 100644
index 00000000..0f14fefe
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/system/password-policy/page.tsx
@@ -0,0 +1,63 @@
+// app/admin/password-policy/page.tsx
+
+import * as React from "react"
+import { Skeleton } from "@/components/ui/skeleton"
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
+import { Separator } from "@/components/ui/separator"
+import { Alert, AlertDescription } from "@/components/ui/alert"
+import { AlertTriangle } from "lucide-react"
+import SecuritySettingsTable from "@/components/system/passwordPolicy"
+import { getSecuritySettings } from "@/lib/password-policy/service"
+
+
+export default async function PasswordPolicyPage() {
+ try {
+ // 보안 설정 데이터 로드
+ const securitySettings = await getSecuritySettings()
+
+ return (
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={4}
+ searchableColumnCount={0}
+ filterableColumnCount={0}
+ cellWidths={["20rem", "30rem", "15rem", "10rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <div className="space-y-6">
+ <div>
+ <h3 className="text-lg font-medium">협력업체 사용자 비밀번호 정책 설정</h3>
+ <p className="text-sm text-muted-foreground">
+ 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다.
+ </p>
+ </div>
+ <Separator />
+ <SecuritySettingsTable initialSettings={securitySettings} />
+ </div>
+ </React.Suspense>
+ )
+ } catch (error) {
+ console.error('Failed to load security settings:', error)
+
+ return (
+ <div className="space-y-6">
+ <div>
+ <h3 className="text-lg font-medium">협력업체 사용자 비밀번호 정책 설정</h3>
+ <p className="text-sm text-muted-foreground">
+ 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다.
+ </p>
+ </div>
+ <Separator />
+ <Alert variant="destructive">
+ <AlertTriangle className="h-4 w-4" />
+ <AlertDescription>
+ 보안 설정을 불러오는 중 오류가 발생했습니다. 페이지를 새로고침하거나 관리자에게 문의하세요.
+ </AlertDescription>
+ </Alert>
+ </div>
+ )
+ }
+} \ No newline at end of file
diff --git a/app/[lng]/partners/(partners)/document-list/layout.tsx b/app/[lng]/partners/(partners)/document-list/layout.tsx
index 0eb9d27b..8d486113 100644
--- a/app/[lng]/partners/(partners)/document-list/layout.tsx
+++ b/app/[lng]/partners/(partners)/document-list/layout.tsx
@@ -22,7 +22,8 @@ export default async function VendorDocuments({
// const vendorId = "17"
const idAsNumber = Number(vendorId)
- const projects = await getVendorProjectsAndContracts(idAsNumber)
+ const projects = await getVendorProjectsAndContracts(idAsNumber);
+ const filteredProjects = projects.filter(v=>v.projectType === "plant")
// 레이아웃 설정 쿠키 가져오기
@@ -39,7 +40,7 @@ export default async function VendorDocuments({
return (
<Shell className="gap-2">
- <VendorDocumentListClient projects={projects}>
+ <VendorDocumentListClient projects={filteredProjects}>
{children}
</VendorDocumentListClient>
</Shell>
diff --git a/app/[lng]/partners/(partners)/settings/layout.tsx b/app/[lng]/partners/(partners)/settings/layout.tsx
new file mode 100644
index 00000000..6f373567
--- /dev/null
+++ b/app/[lng]/partners/(partners)/settings/layout.tsx
@@ -0,0 +1,68 @@
+import { Metadata } from "next"
+
+import { Separator } from "@/components/ui/separator"
+import { SidebarNav } from "@/components/layout/sidebar-nav"
+
+export const metadata: Metadata = {
+ title: "Settings",
+ // description: "Advanced form example using react-hook-form and Zod.",
+}
+
+
+interface SettingsLayoutProps {
+ children: React.ReactNode
+ params: { lng: string }
+}
+
+export default async function SettingsLayout({
+ children,
+ params,
+}: {
+ children: React.ReactNode
+ params: { lng: string }
+}) {
+ const resolvedParams = await params
+ const lng = resolvedParams.lng
+
+
+ const sidebarNavItems = [
+
+ {
+ title: "Account",
+ href: `/${lng}/evcp/settings`,
+ },
+ {
+ title: "Preferences",
+ href: `/${lng}/evcp/settings/preferences`,
+ }
+
+
+ ]
+
+
+ return (
+ <>
+ <div className="container py-6">
+ <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
+ <div className="hidden space-y-6 p-10 pb-16 md:block">
+ <div className="space-y-0.5">
+ <h2 className="text-2xl font-bold tracking-tight">Settings</h2>
+ <p className="text-muted-foreground">
+ Manage your account settings and preferences.
+ </p>
+ </div>
+ <Separator className="my-6" />
+ <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
+ <aside className="-mx-4 lg:w-1/5">
+ <SidebarNav items={sidebarNavItems} />
+ </aside>
+ <div className="flex-1 ">{children}</div>
+ </div>
+ </div>
+ </section>
+ </div>
+
+
+ </>
+ )
+}
diff --git a/app/[lng]/partners/(partners)/settings/page.tsx b/app/[lng]/partners/(partners)/settings/page.tsx
new file mode 100644
index 00000000..d831f0f4
--- /dev/null
+++ b/app/[lng]/partners/(partners)/settings/page.tsx
@@ -0,0 +1,224 @@
+// app/settings/page.tsx (인증 방식 기반 개선된 버전)
+"use client"
+
+import { Separator } from "@/components/ui/separator"
+import { AccountForm } from "@/components/settings/account-form"
+import { SimpleReAuthModal } from "@/components/auth/simple-reauth-modal"
+import { Skeleton } from "@/components/ui/skeleton"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { useSettingsAccess } from "@/hooks/use-settings-access"
+import { Shield, User, Building, ArrowLeft, Mail, Key, Smartphone } from "lucide-react"
+import { useRouter } from "next/navigation"
+import React from "react"
+
+// 인증 방식별 아이콘과 라벨
+const authMethodConfig = {
+ otp: { icon: Smartphone, label: "OTP Authentication", color: "bg-blue-50 text-blue-700 border-blue-300" },
+ email: { icon: Mail, label: "Email Authentication", color: "bg-gray-50 text-gray-700 border-gray-300" },
+ sgips: { icon: Building, label: "S-Gips Enterprise", color: "bg-purple-50 text-purple-700 border-purple-300" },
+ saml: { icon: Key, label: "SAML SSO", color: "bg-green-50 text-green-700 border-green-300" },
+}
+
+export default function SettingsAccountPage() {
+ const router = useRouter()
+ const {
+ accessType,
+ showReAuthModal,
+ isAuthenticated,
+ userEmail,
+ userId,
+ userDomain,
+ authMethod,
+ handleReAuthSuccess,
+ forceReAuth,
+ } = useSettingsAccess({
+ validDuration: 5 * 60 * 1000, // 5분
+ sgipsRedirectPath: "/partners/dashboard",
+ })
+
+ // 로딩 상태
+ if (accessType === 'loading') {
+ return (
+ <div className="space-y-6">
+ <div>
+ <Skeleton className="h-7 w-24" />
+ <Skeleton className="h-4 w-96 mt-2" />
+ </div>
+ <Separator />
+ <div className="space-y-4">
+ <Skeleton className="h-10 w-full" />
+ <Skeleton className="h-10 w-full" />
+ <Skeleton className="h-10 w-full" />
+ </div>
+ </div>
+ )
+ }
+
+ // 인증되지 않은 상태
+ if (accessType === 'unauthenticated') {
+ return (
+ <div className="text-center py-12">
+ <div className="mx-auto h-12 w-12 rounded-full bg-red-100 flex items-center justify-center mb-4">
+ <User className="h-6 w-6 text-red-600" />
+ </div>
+ <h3 className="text-lg font-medium mb-2">Authentication Required</h3>
+ <p className="text-muted-foreground mb-4">
+ Please sign in to access account settings.
+ </p>
+ <Button onClick={() => router.push("/auth/login")}>
+ Sign In
+ </Button>
+ </div>
+ )
+ }
+
+ // S-Gips 사용자 접근 차단
+ if (accessType === 'blocked_sgips') {
+ return (
+ <div className="text-center py-12">
+ <div className="mx-auto h-12 w-12 rounded-full bg-purple-100 flex items-center justify-center mb-4">
+ <Building className="h-6 w-6 text-purple-600" />
+ </div>
+ <h3 className="text-lg font-medium mb-2">Enterprise Account</h3>
+ <p className="text-muted-foreground mb-2">
+ Your account is managed through S-Gips enterprise system.
+ </p>
+ <p className="text-sm text-muted-foreground mb-4">
+ Domain: <span className="font-medium">{userDomain}</span>
+ </p>
+ <Button
+ onClick={() => router.push("/dashboard")}
+ className="flex items-center gap-2"
+ >
+ <ArrowLeft className="h-4 w-4" />
+ Back to Dashboard
+ </Button>
+ </div>
+ )
+ }
+
+ // 재인증 대기 상태
+ if (accessType === 'reauth_required') {
+ return (
+ <div className="space-y-6">
+ <div>
+ <h3 className="text-lg font-medium">Account Settings</h3>
+ <p className="text-sm text-muted-foreground">
+ Update your account settings and manage your profile information.
+ </p>
+ </div>
+ <Separator />
+
+ <div className="text-center py-12">
+ <div className="mx-auto h-16 w-16 rounded-full bg-amber-100 flex items-center justify-center mb-4">
+ <Shield className="h-8 w-8 text-amber-600 animate-pulse" />
+ </div>
+ <h3 className="text-lg font-medium mb-2">Security Verification Required</h3>
+ <p className="text-muted-foreground mb-4">
+ Please verify your password to access account settings.
+ </p>
+
+ {/* 인증 방식 표시 */}
+ {authMethod && authMethodConfig[authMethod] && (
+ <div className="flex items-center justify-center gap-2 mb-4">
+ {React.createElement(authMethodConfig[authMethod].icon, {
+ className: "h-4 w-4 text-muted-foreground"
+ })}
+ <Badge variant="outline" className={authMethodConfig[authMethod].color}>
+ {authMethodConfig[authMethod].label}
+ </Badge>
+ </div>
+ )}
+ </div>
+
+ <SimpleReAuthModal
+ isOpen={showReAuthModal}
+ onSuccess={handleReAuthSuccess}
+ userEmail={userEmail}
+ />
+ </div>
+ )
+ }
+
+ // 접근 허용 상태
+ const currentAuthConfig = authMethod && authMethodConfig[authMethod]
+
+ return (
+ <div className="space-y-6">
+ <div>
+ <div className="flex items-center justify-between">
+ <div>
+ <h3 className="text-lg font-medium">Account Settings</h3>
+ <p className="text-sm text-muted-foreground">
+ Update your account settings and manage your profile information.
+ </p>
+ </div>
+
+ {/* 보안 상태 표시 */}
+ <div className="flex items-center gap-2">
+ <div className="flex items-center gap-2 px-3 py-1 bg-green-50 text-green-700 rounded-full text-sm">
+ <Shield className="h-4 w-4" />
+ <span>Verified</span>
+ </div>
+ </div>
+ </div>
+
+ {/* 사용자 정보 및 인증 방식 표시 */}
+ <div className="mt-4 p-4 bg-slate-50 border border-slate-200 rounded-lg">
+ <div className="flex items-center justify-between">
+ <div className="space-y-2">
+ <div className="flex items-center gap-3">
+ <div className="flex items-center gap-2">
+ <div className="h-2 w-2 bg-green-500 rounded-full"></div>
+ <span className="text-sm font-medium text-slate-900">
+ {userEmail}
+ </span>
+ </div>
+
+ {/* 인증 방식 뱃지 */}
+ {currentAuthConfig && (
+ <div className="flex items-center gap-1">
+ {React.createElement(currentAuthConfig.icon, {
+ className: "h-3 w-3"
+ })}
+ <Badge variant="outline" className={`text-xs ${currentAuthConfig.color}`}>
+ {currentAuthConfig.label}
+ {userDomain && authMethod === 'saml' && ` (${userDomain})`}
+ </Badge>
+ </div>
+ )}
+ </div>
+
+ {/* 도메인 정보 */}
+ {userDomain && (
+ <div className="text-xs text-slate-600">
+ Domain: <span className="font-medium">{userDomain}</span>
+ </div>
+ )}
+ </div>
+
+ <div className="flex items-center gap-2">
+ {/* 이메일 인증 사용자만 재인증 버튼 표시 */}
+ {authMethod === 'email' && (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={forceReAuth}
+ className="text-amber-700 border-amber-300 hover:bg-amber-50"
+ >
+ Re-verify
+ </Button>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <Separator />
+
+ {/* 메인 콘텐츠 */}
+ <AccountForm />
+ </div>
+ )
+} \ No newline at end of file
diff --git a/app/[lng]/partners/(partners)/settings/preferences/page.tsx b/app/[lng]/partners/(partners)/settings/preferences/page.tsx
new file mode 100644
index 00000000..e2a88021
--- /dev/null
+++ b/app/[lng]/partners/(partners)/settings/preferences/page.tsx
@@ -0,0 +1,17 @@
+import { Separator } from "@/components/ui/separator"
+import { AppearanceForm } from "@/components/settings/appearance-form"
+
+export default function SettingsAppearancePage() {
+ return (
+ <div className="space-y-6">
+ <div>
+ <h3 className="text-lg font-medium">Preference</h3>
+ <p className="text-sm text-muted-foreground">
+ Customize the preference of the app.
+ </p>
+ </div>
+ <Separator />
+ <AppearanceForm />
+ </div>
+ )
+}
diff --git a/app/[lng]/partners/(partners)/system/layout.tsx b/app/[lng]/partners/(partners)/system/layout.tsx
new file mode 100644
index 00000000..504570bb
--- /dev/null
+++ b/app/[lng]/partners/(partners)/system/layout.tsx
@@ -0,0 +1,71 @@
+import { Metadata } from "next"
+
+import { Separator } from "@/components/ui/separator"
+import { SidebarNav } from "@/components/layout/sidebar-nav"
+
+export const metadata: Metadata = {
+ title: "System Setting",
+ // description: "Advanced form example using react-hook-form and Zod.",
+}
+
+
+interface SettingsLayoutProps {
+ children: React.ReactNode
+ params: { lng: string }
+}
+
+export default async function SettingsLayout({
+ children,
+ params,
+}: {
+ children: React.ReactNode
+ params: { lng: string }
+}) {
+ const resolvedParams = await params
+ const lng = resolvedParams.lng
+
+
+ const sidebarNavItems = [
+
+ {
+ title: "사용자",
+ href: `/${lng}/evcp/system`,
+ },
+ {
+ title: "Roles",
+ href: `/${lng}/evcp/system/roles`,
+ },
+ {
+ title: "권한 통제",
+ href: `/${lng}/evcp/system/permissions`,
+ },
+
+ ]
+
+
+ return (
+ <>
+ <div className="container py-6">
+ <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
+ <div className="hidden space-y-6 p-10 pb-16 md:block">
+ <div className="space-y-0.5">
+ <h2 className="text-2xl font-bold tracking-tight">시스템 설정</h2>
+ <p className="text-muted-foreground">
+ 사용자, 롤, 접근 권한을 관리하세요.
+ </p>
+ </div>
+ <Separator className="my-6" />
+ <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
+ <aside className="-mx-4 lg:w-1/5">
+ <SidebarNav items={sidebarNavItems} />
+ </aside>
+ <div className="flex-1 ">{children}</div>
+ </div>
+ </div>
+ </section>
+ </div>
+
+
+ </>
+ )
+}
diff --git a/app/[lng]/partners/(partners)/system/page.tsx b/app/[lng]/partners/(partners)/system/page.tsx
index a1e9f8be..1224851b 100644
--- a/app/[lng]/partners/(partners)/system/page.tsx
+++ b/app/[lng]/partners/(partners)/system/page.tsx
@@ -1,8 +1,56 @@
+import { Separator } from "@/components/ui/separator"
+import { type SearchParams } from "@/types/table"
+import * as React from "react"
+import { getValidFilters } from "@/lib/data-table"
+import { searchParamsCache } from "@/lib/admin-users/validations"
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
+import { AdmUserTable } from "@/lib/admin-users/table/ausers-table"
+import { getAllRolesbyVendor, getUserCountGroupByRoleAndVendor, getVendorUsers } from "@/lib/vendor-users/service"
+import { VendorUserTable } from "@/lib/vendor-users/table/ausers-table"
-export default function Pages() {
- return (
- <>
- test
- </>
- )
- } \ No newline at end of file
+interface IndexPageProps {
+ searchParams: Promise<SearchParams>
+}
+
+export default async function SystemUserPage(props: IndexPageProps) {
+
+ const searchParams = await props.searchParams
+ const search = searchParamsCache.parse(searchParams)
+
+ const validFilters = getValidFilters(search.filters)
+
+ const promises = Promise.all([
+ getVendorUsers({
+ ...search,
+ filters: validFilters,
+ }),
+ getUserCountGroupByRoleAndVendor(),
+ getAllRolesbyVendor()
+ ])
+
+ return (
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={6}
+ searchableColumnCount={1}
+ filterableColumnCount={2}
+ cellWidths={["10rem", "12rem", "12rem", "12rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <div className="space-y-6">
+ <div>
+ <h3 className="text-lg font-medium">Users</h3>
+ <p className="text-sm text-muted-foreground">
+ 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다.
+ </p>
+ </div>
+ <Separator />
+ <VendorUserTable promises={promises} />
+ </div>
+ </React.Suspense>
+
+ )
+} \ No newline at end of file
diff --git a/app/[lng]/partners/(partners)/system/permissions/page.tsx b/app/[lng]/partners/(partners)/system/permissions/page.tsx
new file mode 100644
index 00000000..fe33f920
--- /dev/null
+++ b/app/[lng]/partners/(partners)/system/permissions/page.tsx
@@ -0,0 +1,17 @@
+import PermissionsTreeVendor from "@/components/system/permissionsTreeVendor"
+import { Separator } from "@/components/ui/separator"
+
+export default function PermissionsPage() {
+ return (
+ <div className="space-y-6">
+ <div>
+ <h3 className="text-lg font-medium">Permissions</h3>
+ <p className="text-sm text-muted-foreground">
+ Set permissions to the menu by Role
+ </p>
+ </div>
+ <Separator />
+ <PermissionsTreeVendor/>
+ </div>
+ )
+}
diff --git a/app/[lng]/partners/(partners)/system/roles/page.tsx b/app/[lng]/partners/(partners)/system/roles/page.tsx
new file mode 100644
index 00000000..fe074600
--- /dev/null
+++ b/app/[lng]/partners/(partners)/system/roles/page.tsx
@@ -0,0 +1,68 @@
+import * as React from "react"
+import { type SearchParams } from "@/types/table"
+
+import { getValidFilters } from "@/lib/data-table"
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
+import { Separator } from "@/components/ui/separator"
+
+import { searchParamsCache } from "@/lib/roles/validations"
+import { searchParamsCache as searchParamsCache2 } from "@/lib/admin-users/validations"
+import { RolesTable } from "@/lib/roles/table/roles-table"
+import { getRolesWithCount } from "@/lib/roles/services"
+import { getUsersAll } from "@/lib/users/service"
+
+interface IndexPageProps {
+ searchParams: Promise<SearchParams>
+}
+
+export default async function UserTable(props: IndexPageProps) {
+ const searchParams = await props.searchParams
+ const search = searchParamsCache.parse(searchParams)
+ const search2 = searchParamsCache2.parse(searchParams)
+
+ const validFilters = getValidFilters(search.filters)
+
+ const promises = Promise.all([
+ getRolesWithCount({
+ ...search,
+ filters: validFilters,
+ }),
+
+
+ ])
+
+
+ const promises2 = Promise.all([
+ getUsersAll({
+ ...search2,
+ filters: validFilters,
+ }, "evcp"),
+ ])
+
+
+ return (
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={6}
+ searchableColumnCount={1}
+ filterableColumnCount={2}
+ cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <div className="space-y-6">
+ <div>
+ <h3 className="text-lg font-medium">Role Management</h3>
+ <p className="text-sm text-muted-foreground">
+ 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다.
+ </p>
+ </div>
+ <Separator />
+ <RolesTable promises={promises} promises2={promises2} />
+ </div>
+ </React.Suspense>
+
+ )
+}
diff --git a/app/[lng]/privacy/page.tsx b/app/[lng]/privacy/page.tsx
new file mode 100644
index 00000000..28881db2
--- /dev/null
+++ b/app/[lng]/privacy/page.tsx
@@ -0,0 +1,5 @@
+import { PrivacyPolicyPage } from "@/components/login/privacy-policy-page"
+
+export default function Privacy() {
+ return <PrivacyPolicyPage />
+} \ No newline at end of file